package typeexpr

import (
    "testing"

    "Havoc/pkg/profile/yaotl/gohcl"

    "Havoc/pkg/profile/yaotl"
    "Havoc/pkg/profile/yaotl/hclsyntax"
    "Havoc/pkg/profile/yaotl/json"
    "github.com/zclconf/go-cty/cty"
)

func TestGetType(t *testing.T) {
    tests := []struct {
        Source     string
        Constraint bool
        Want       cty.Type
        WantError  string
    }{
        // keywords
        {
            `bool`,
            false,
            cty.Bool,
            "",
        },
        {
            `number`,
            false,
            cty.Number,
            "",
        },
        {
            `string`,
            false,
            cty.String,
            "",
        },
        {
            `any`,
            false,
            cty.DynamicPseudoType,
            `The keyword "any" cannot be used in this type specification: an exact type is required.`,
        },
        {
            `any`,
            true,
            cty.DynamicPseudoType,
            "",
        },
        {
            `list`,
            false,
            cty.DynamicPseudoType,
            "The list type constructor requires one argument specifying the element type.",
        },
        {
            `map`,
            false,
            cty.DynamicPseudoType,
            "The map type constructor requires one argument specifying the element type.",
        },
        {
            `set`,
            false,
            cty.DynamicPseudoType,
            "The set type constructor requires one argument specifying the element type.",
        },
        {
            `object`,
            false,
            cty.DynamicPseudoType,
            "The object type constructor requires one argument specifying the attribute types and values as a map.",
        },
        {
            `tuple`,
            false,
            cty.DynamicPseudoType,
            "The tuple type constructor requires one argument specifying the element types as a list.",
        },

        // constructors
        {
            `bool()`,
            false,
            cty.DynamicPseudoType,
            `Primitive type keyword "bool" does not expect arguments.`,
        },
        {
            `number()`,
            false,
            cty.DynamicPseudoType,
            `Primitive type keyword "number" does not expect arguments.`,
        },
        {
            `string()`,
            false,
            cty.DynamicPseudoType,
            `Primitive type keyword "string" does not expect arguments.`,
        },
        {
            `any()`,
            false,
            cty.DynamicPseudoType,
            `Primitive type keyword "any" does not expect arguments.`,
        },
        {
            `any()`,
            true,
            cty.DynamicPseudoType,
            `Primitive type keyword "any" does not expect arguments.`,
        },
        {
            `list(string)`,
            false,
            cty.List(cty.String),
            ``,
        },
        {
            `set(string)`,
            false,
            cty.Set(cty.String),
            ``,
        },
        {
            `map(string)`,
            false,
            cty.Map(cty.String),
            ``,
        },
        {
            `list()`,
            false,
            cty.DynamicPseudoType,
            `The list type constructor requires one argument specifying the element type.`,
        },
        {
            `list(string, string)`,
            false,
            cty.DynamicPseudoType,
            `The list type constructor requires one argument specifying the element type.`,
        },
        {
            `list(any)`,
            false,
            cty.List(cty.DynamicPseudoType),
            `The keyword "any" cannot be used in this type specification: an exact type is required.`,
        },
        {
            `list(any)`,
            true,
            cty.List(cty.DynamicPseudoType),
            ``,
        },
        {
            `object({})`,
            false,
            cty.EmptyObject,
            ``,
        },
        {
            `object({name=string})`,
            false,
            cty.Object(map[string]cty.Type{"name": cty.String}),
            ``,
        },
        {
            `object({"name"=string})`,
            false,
            cty.EmptyObject,
            `Object constructor map keys must be attribute names.`,
        },
        {
            `object({name=nope})`,
            false,
            cty.Object(map[string]cty.Type{"name": cty.DynamicPseudoType}),
            `The keyword "nope" is not a valid type specification.`,
        },
        {
            `object()`,
            false,
            cty.DynamicPseudoType,
            `The object type constructor requires one argument specifying the attribute types and values as a map.`,
        },
        {
            `object(string)`,
            false,
            cty.DynamicPseudoType,
            `Object type constructor requires a map whose keys are attribute names and whose values are the corresponding attribute types.`,
        },
        {
            `tuple([])`,
            false,
            cty.EmptyTuple,
            ``,
        },
        {
            `tuple([string, bool])`,
            false,
            cty.Tuple([]cty.Type{cty.String, cty.Bool}),
            ``,
        },
        {
            `tuple([nope])`,
            false,
            cty.Tuple([]cty.Type{cty.DynamicPseudoType}),
            `The keyword "nope" is not a valid type specification.`,
        },
        {
            `tuple()`,
            false,
            cty.DynamicPseudoType,
            `The tuple type constructor requires one argument specifying the element types as a list.`,
        },
        {
            `tuple(string)`,
            false,
            cty.DynamicPseudoType,
            `Tuple type constructor requires a list of element types.`,
        },
        {
            `shwoop(string)`,
            false,
            cty.DynamicPseudoType,
            `Keyword "shwoop" is not a valid type constructor.`,
        },
        {
            `list("string")`,
            false,
            cty.List(cty.DynamicPseudoType),
            `A type specification is either a primitive type keyword (bool, number, string) or a complex type constructor call, like list(string).`,
        },

        // More interesting combinations
        {
            `list(object({}))`,
            false,
            cty.List(cty.EmptyObject),
            ``,
        },
        {
            `list(map(tuple([])))`,
            false,
            cty.List(cty.Map(cty.EmptyTuple)),
            ``,
        },
    }

    for _, test := range tests {
        t.Run(test.Source, func(t *testing.T) {
            expr, diags := hclsyntax.ParseExpression([]byte(test.Source), "", hcl.Pos{Line: 1, Column: 1})
            if diags.HasErrors() {
                t.Fatalf("failed to parse: %s", diags)
            }

            got, diags := getType(expr, test.Constraint)
            if test.WantError == "" {
                for _, diag := range diags {
                    t.Error(diag)
                }
            } else {
                found := false
                for _, diag := range diags {
                    t.Log(diag)
                    if diag.Severity == hcl.DiagError && diag.Detail == test.WantError {
                        found = true
                    }
                }
                if !found {
                    t.Errorf("missing expected error detail message: %s", test.WantError)
                }
            }

            if !got.Equals(test.Want) {
                t.Errorf("wrong result\ngot:  %#v\nwant: %#v", got, test.Want)
            }
        })
    }
}

func TestGetTypeJSON(t *testing.T) {
    // We have fewer test cases here because we're mainly exercising the
    // extra indirection in the JSON syntax package, which ultimately calls
    // into the native syntax parser (which we tested extensively in
    // TestGetType).
    tests := []struct {
        Source     string
        Constraint bool
        Want       cty.Type
        WantError  string
    }{
        {
            `{"expr":"bool"}`,
            false,
            cty.Bool,
            "",
        },
        {
            `{"expr":"list(bool)"}`,
            false,
            cty.List(cty.Bool),
            "",
        },
        {
            `{"expr":"list"}`,
            false,
            cty.DynamicPseudoType,
            "The list type constructor requires one argument specifying the element type.",
        },
    }

    for _, test := range tests {
        t.Run(test.Source, func(t *testing.T) {
            file, diags := json.Parse([]byte(test.Source), "")
            if diags.HasErrors() {
                t.Fatalf("failed to parse: %s", diags)
            }

            type TestContent struct {
                Expr hcl.Expression `hcl:"expr"`
            }
            var content TestContent
            diags = gohcl.DecodeBody(file.Body, nil, &content)
            if diags.HasErrors() {
                t.Fatalf("failed to decode: %s", diags)
            }

            got, diags := getType(content.Expr, test.Constraint)
            if test.WantError == "" {
                for _, diag := range diags {
                    t.Error(diag)
                }
            } else {
                found := false
                for _, diag := range diags {
                    t.Log(diag)
                    if diag.Severity == hcl.DiagError && diag.Detail == test.WantError {
                        found = true
                    }
                }
                if !found {
                    t.Errorf("missing expected error detail message: %s", test.WantError)
                }
            }

            if !got.Equals(test.Want) {
                t.Errorf("wrong result\ngot:  %#v\nwant: %#v", got, test.Want)
            }
        })
    }
}
